<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>虚拟列表</title>
    <style>
        .listview {
            height: 220px;
            width: 400px;
            position: relative;
            overflow-y: auto;
            box-sizing: border-box;
            border: 1px solid #dbdbdb;
        }

        .placeholder {
            position: absolute;
            top: 0;
            left: 0;
            right: 0;
            z-index: -1;
            height: 400px;
            /* background: linear-gradient(#badaf3, #0575ca); */
        }

        .listcontent {
            list-style: none;
            margin: 0;
            padding: 0;
        }

        .listcontent li:nth-of-type(odd) {
            background-color: #f5f5f5;
        }

        .listcontent li:nth-of-type(even) {
            background-color: #e9e9e9;
        }
    </style>
</head>

<body>
    <div id="listview" class="listview">
        <div id="placeholder" class="placeholder"></div>
        <ul id="listcontent" class="listcontent">
        </ul>
    </div>
    <script>
        const itemHeight = 50; // 每条数据高度
        const bufferSize = 5; // 前后多渲染几条数据，滚动时看起来更自然
        const listview = document.querySelector('#listview');
        const listcontent = document.querySelector('#listcontent');
        const placeholder = document.querySelector('#placeholder');
        let myData = loadData();
        placeholder.style.height = `${myData.length * itemHeight}px`;
        let itemCount = Math.ceil(listview.clientHeight / itemHeight);
        let startCurrent = 0;
        renderData(myData, startCurrent, Math.min(myData.length, itemCount + bufferSize));
        listview.addEventListener('scroll', scrollHandle);

        function scrollHandle(e) {
            let scrollTop = listview.scrollTop;
            start = Math.floor(scrollTop / itemHeight);
            end = start + itemCount;
            start = Math.max(0, start - bufferSize);
            end = Math.min(myData.length, end + bufferSize);
            // 当滚动距离较小，需要显示的首条数据不变时，无需重新渲染数据，
            if (start != startCurrent) {
                renderData(myData, start, end);
                startCurrent = start;
            }
            listcontent.style.transform = `translateY(${start * itemHeight}px)`;
        }

        // 模拟请求数据
        function loadData() {
            let data = [];
            for (let i = 0; i < 100; i++) {
                data.push(i.toString())
            }
            return data;
        }

        //渲染数据
        function renderData(data, start, end) {
            let fragment = document.createDocumentFragment();
            for (let i = start; i < end; i++) {
                let li = document.createElement('li');
                li.textContent = data[i].toString();
                li.style.height = itemHeight + 'px';
                fragment.appendChild(li);
            }
            listcontent.replaceChildren();
            listcontent.appendChild(fragment);
        }
    </script>
</body>

</html>